Skip to content

Conversation

jakiestfu
Copy link
Contributor

@jakiestfu jakiestfu commented Sep 25, 2025

@AlemTuzlak Hello again! I have built a feature to render the devtools in a shadow dom node. This ensures that styles are not inherited from the parent page. However, there are some caveats and/or things I'm not 100% certain I understand, and wanted to pose this PR to perhaps start those discussions.

As you can see, I've added some styles to the basic example that aggressively target the styles of the devtools UI. After the change, there is no interference.

Before After
before after

Copy link

changeset-bot bot commented Sep 25, 2025

🦋 Changeset detected

Latest commit: 4b3b456

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@tanstack/react-devtools Minor
@tanstack/devtools Minor
@tanstack/solid-devtools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

pkg-pr-new bot commented Sep 25, 2025

More templates

@tanstack/devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools@181

@tanstack/devtools-ui

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-ui@181

@tanstack/devtools-utils

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-utils@181

@tanstack/devtools-vite

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-vite@181

@tanstack/devtools-event-bus

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-bus@181

@tanstack/devtools-event-client

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-client@181

@tanstack/react-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/react-devtools@181

@tanstack/solid-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/solid-devtools@181

commit: 4b3b456

observer.disconnect()
})
}
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So "syncing" the styles from the parent page to the shadow DOM node is not what I initially set out to do. In fact, there were two approaches I made prior to this, but both failed.

  • Approach 1: Leverage goobers css.bind({ target: <element> }) feature.

    • This basically involves creating a context provider that binds the CSS target and passes that down to child components to use.
    • This looked like a useGoober hook in the UI library, that provides the CSS bound target for the shadow node
    • Just did not work. Even when binding the target manually, goober always appended the styles to the head of the owner document and not the shadow root. Both UI and devtool packages were implementing the context packages correctly.
    • Reference material, Goober Docs
    • Perhaps this is worth a revisit
  • Approach 2: Just do Goobers extractCSS

    • Only works on the server 🤷

}))
}

setPluginContainers((prev) => ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are rendering the component in a shadow DOM node, e.ownerDocument.getElementById(id) always returns null and the plugins are never mounted.

I'm not certain I understand what this if statement guards against since I thought the elements were defined by the devtools plugin itself. Shouldn't they always exist?

@jakiestfu jakiestfu changed the title feat(): render devtools in a shadow node feat: render devtools in a shadow node Sep 25, 2025
@jakiestfu
Copy link
Contributor Author

CC @TakhyunKim @colelawrence

@AlemTuzlak
Copy link
Collaborator

Thank you for the PR, I'll review it in more details as soon as I can, before then, what exactly would be the benefit of rendering it in a shadow dom besides escaping global styling?

@jakiestfu
Copy link
Contributor Author

jakiestfu commented Sep 29, 2025

Of course, @AlemTuzlak!

I honestly haven't really thought about any other benefits but I guess this is what I found after a little research:

Beyond that, I'm not sure what additional benefits we receive. In all honesty, I would hope style isolation would be an absolute requirement for this package if its intended to be used across any application. I've tried to introduce this into our organization, but our CSS reset is getting in the way of the styles in the devtools and things are quite visually broken.

@jakiestfu
Copy link
Contributor Author

As an FYI, I was able to end up binding the goober css function to render inside of the shadow node more idiomatically (using context throughout the codebase for css) but this only solves styles for devtools and devtools-ui. Unfortunately, additional packages like @tanstack/react-query-devtools and @tanstack/react-router-devtools just default to their current document context and still end up appending their styles to the head of the document instead of inside the shadow node.

All this to say, the current solution (in this PR) syncs styles from all goober usage on the page, to within the shadow root and it works perfectly fine. The code for the pip styling was inspiration for this.

This was the proof-of-concept for the context stuff if you cared to see, but it seems like it would be a pain to continue down this route.

// packages/devtools-ui/src/components/shadow-root.tsx

import { createContext, onCleanup, onMount, useContext } from 'solid-js'
import { render } from 'solid-js/web'
import { css as gooberCss } from 'goober'
import type { ParentComponent } from 'solid-js'

export type CssFn = typeof gooberCss
type GooberCtx = { css: CssFn }

const GooberContext = createContext<GooberCtx>()

export const useCss = () => {
  const ctx = useContext(GooberContext)
  if (!ctx) throw new Error('useCss must be used inside <ShadowRoot>')
  return ctx
}

export const ShadowRoot: ParentComponent = (props) => {
  let host!: HTMLDivElement

  onMount(() => {
    const shadow = host.attachShadow({ mode: 'open' })
    const ctx: GooberCtx = { css: gooberCss.bind({ target: shadow }) }

    const dispose = render(
      () => (
        <GooberContext.Provider value={ctx}>
          {props.children}
        </GooberContext.Provider>
      ),
      shadow,
    )
    onCleanup(dispose)
  })

  return <div ref={host} />
}

@colelawrence
Copy link
Contributor

I had a question @jakiestfu - I already started using the DevTools to be a frame for some existing UI pieces I'm using in my app. Does this affect that use case? I'd rather not have to muck around with css injection or something to get around the shadow dom.

@jakiestfu
Copy link
Contributor Author

jakiestfu commented Oct 1, 2025

Do you mean this frame, @colelawrence? If so, I have no idea. I could explore your use-cases for you if you wanted to share a little more or high-level it for me.

MDN does note frame is a deprecated feature and Shadow DOM is shiny and new and stuff 🤷

And if by chance you mean iframe, that comes with its own complexities regarding postMessage and stuff. Unless they're on the same origin.


I'd rather not have to muck around with css injection

We already do that for the PIP window though because we're using Goober 🥺

@jakiestfu
Copy link
Contributor Author

I apologize in advance for my eagerness to have this shipped or explored by the maintainers. The lack of style sandboxing is fully preventing us at Turo from adopting devtools internally as the styles are well... broken

Stronger CSS styling could help this problem, but that pushes the work and effort unto the end-users of the devtools package as opposed to the package itself handling this.

What can I do for ya'll to help support your efforts?

@AlemTuzlak
Copy link
Collaborator

Sorry I had some deadlines I needed to hit so I had no time to look at this, will get to it this week definitely

@AlemTuzlak
Copy link
Collaborator

I've been looking at this, and I have no idea how it works with the change you did to the react-devtools adapter but here are a few things:

  • i don't want this behavior to be the default one but rather a config option you can pass in, eg shadowDOMElement to mount to
  • this also needs to be changed I guess with the title in the react-devtools

@jakiestfu
Copy link
Contributor Author

@AlemTuzlak Thank you kindly for your feedback. I'll address your comments:

I've been looking at this, and I have no idea how it works with the change you did to the react-devtools adapter but here are a few things:

If you are referring to this code:

If we render the devtools into a shadow node, const target = e.ownerDocument.getElementById(id) returns undefined instead of the container element to render the DOM node into.

Thing is, the container should always be present since thats being rendered by the devtools itself, so I removed that line so it allows the parent devtools to still render components whilst in the shadow node. I think this is just a potentially unnecessary safe-guard, but I could be wrong.

I don't want this behavior to be the default one but rather a config option you can pass in, eg shadowDOMElement to mount to

This is not sufficient because the way Goober is used, it just blindly renders styles to the parent HTML page. Thats why styles are (currently) synced from the main page to the PIP window, and why they'd need to be synced to the Shadow now as well. If the core devtools (and all subsequent plugins) supported more options about where to render things, it could work, but this should be a feature that should "just work" without end-users having to worry about the implications of how we render it.

I know that react-query-devtools supports passing a shadow DOM target but this would require quite a large refactor to support with the current devtools architecture. We'd need access to the container that elements need to mount into in the plugins array and things begin to feel messy.

This also needs to be changed I guess with the title in the react-devtools
Do you mind expanding upon this more? I'm not sure I quite follow.


All this to say, if you feel this is a bigger architectural/code change than you'd like to make right now, perhaps I can submit a PR that tightens up styles in the devtools so things aren't as visually broken for our use-cases. I just feat we'll always have to compete with styles from the outer page in the child components.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants